package org.springframework.validation;

import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.jsp.el.ELException;
import javax.servlet.jsp.el.ExpressionEvaluator;
import javax.servlet.jsp.el.FunctionMapper;
import javax.servlet.jsp.el.VariableResolver;

import org.apache.commons.el.ExpressionEvaluatorImpl;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.beanvalidation.CustomValidatorBean;
import org.springframework.validation.executors.RuleExecutor;
import org.springframework.validation.executors.RuleExecutorContainer;
import org.springframework.validation.rules.ValidationRule;
import org.springframework.validation.rules.ValidationRulesContainer;
import org.springframework.validation.util.RequestUtils;

public abstract class ValidationManager extends CustomValidatorBean  {
	
	protected static Log log = LogFactory.getLog(ValidationManager.class);
	
	@Autowired
	protected ValidationRulesContainer rulesContainer;
	
	@Autowired
	protected RuleExecutorContainer ruleExecutorContainer;
	
	@Autowired 
	protected MessageSource messageSource;
	
	@Override
	public boolean supports(Class<?> clazz) {
		return rulesContainer.supportsClass(clazz);
	}
	
	@Override
	public void validate(Object model, Errors errors) {
		validateContextRules(model, errors);
		validateModelRules(model, errors, new ArrayList<Object>());
	}

	protected void validateModelRules(Object model, Errors errors, List<Object> checkedModels) {
		// to prevent unneeded recursion:
		// if the model is null,
		// or the model is a proxy [e.g. hibernate] object,
		// or the model has already been checked,
		// skip it.
		if (model == null 
			|| model.getClass().getName().contains("javassist") 
			|| checkedModels.contains(model)) {
			return;
		} else {
			checkedModels.add(model);
		}
		
		List<ValidationRule> modelRules = rulesContainer.getModelRules(model.getClass());
		callModelRules(model, errors, modelRules);
		 
		// Get fields for subbeans and iterate
		BeanWrapperImpl beanWrapper = new BeanWrapperImpl(model);
		PropertyDescriptor[] propertyDescriptors = beanWrapper.getPropertyDescriptors(); 
		for (PropertyDescriptor property : propertyDescriptors) {
			if (rulesContainer.supportsClass(property.getPropertyType())) {
				Object subBean = beanWrapper.getPropertyValue(property.getName());
				errors.pushNestedPath(property.getName());
				validateModelRules(subBean, errors, checkedModels);
				errors.popNestedPath();
			}
		}
	}
	
	protected void callModelRules(Object model, Errors errors, List<ValidationRule> modelRules) {
		if (modelRules == null) {
			return;
		}
		for (ValidationRule rule : modelRules) {
			
			// get full path to current model
			String fullPath = appendPath(errors.getNestedPath(), rule.getPath());
			
			// if this field is not on the page we're checking,
			// or the field already has errors, skip it.		

			if (!RequestUtils.getRequest().getParameterMap().containsKey(fullPath)
				|| errors.hasFieldErrors(rule.getPath())) {
				continue;
			}
			
			// update rule for full path
			ValidationRule modelRule = rule.cloneWithPath(fullPath);
			
			if (passes(modelRule, model)) {
				// If the rule passes and it has children,
				// it is a condition for nested elements.
				// Call children instead.
				if (modelRule.hasChildren()) {
					callModelRules(model, errors, modelRule.getRules());
				}
			} else {
				// If the rule fails and it has children,
				// it is a condition for nested elements.
				// Skip nested elements.
				if (modelRule.hasChildren()) {
					continue;
				} else {
					// If the rule has no children and fails,
					// perform fail action.
					logError(modelRule, model, errors);
				}
			}
		}
	}
	
	protected void validateContextRules(Object model, Errors errors) {
		List<ValidationRule> contextRules = getContextRules(model);
		callContextRules(model, errors, contextRules);
	}
	
	protected abstract List<ValidationRule> getContextRules(Object model);
	
	protected void callContextRules(Object model, Errors errors, List<ValidationRule> contextRules) {
		if (contextRules == null || contextRules.isEmpty()) {
			return;
		}
		for (ValidationRule rule : contextRules) {
			if (errors.hasFieldErrors(rule.getPath())) {
				continue;
			}
			
			if (passes(rule, model)) {
				// If the rule passes and it has children,
				// it is a condition for nested elements.
				// Call children instead.
				if (rule.hasChildren()) {
					callContextRules(model, errors, rule.getRules());
				}
			} else {
				// If the rule fails and it has children,
				// it is a condition for nested elements.
				// Skip nested elements.
				if (rule.hasChildren()) {
					continue;
				} else {
					// If the rule has no children and fails,
					// perform fail action.
					logError(rule, model, errors);
				}
			}
		}
	}
	
	protected boolean passes(ValidationRule rule, Object rootModel) {			
		// get args
		Object ruleModel = getContextModel(rootModel, rule.getPath());
		Object ruleArg = getContextArgument(rootModel, rule.getValue());

		// call method
		boolean isValid;
		RuleExecutor executor = ruleExecutorContainer.getRuleExecutorByName(rule.getType());
		try {
			isValid = executor.validate(ruleModel, ruleArg);
		} catch (Exception ve) {
			throw new RuntimeException("Error occured during validation: ", ve);
		}
		return isValid;
	}
	
	protected Object getContextModel(Object model, String expression) {
		Object result = null;
		if (expression == null || expression.isEmpty()) {
			return null;
		}
		if (isEL(expression)) {
			result = resolveEL(expression, model);
		} else {
			BeanWrapperImpl beanWrapper = new BeanWrapperImpl(model);
			if (model != null && beanWrapper.isReadableProperty(expression)) {
				result = beanWrapper.getPropertyValue(expression);
			}
		}
		return result;
	}
	
	protected Object getContextArgument(Object model, String expression) {
		Object result = null;
		if (expression == null || expression.isEmpty()) {
			return null;
		}
		if (isEL(expression)) {
			result = resolveEL(expression, model);
		} else {
			result = expression;
		}
		return result;
	}
	
	protected void logError(ValidationRule rule, Object rootModel, Errors errors) {
		String errorMessageKey = rule.getMessage();
        if (errorMessageKey == null || errorMessageKey.isEmpty()) {
                errorMessageKey = "springvalidation.errors." + rule.getType();
        }
        String errorMessagePath = rule.getErrorPath();
        if (errorMessagePath == null || errorMessagePath.isEmpty()) {
                errorMessagePath = rule.getPath();
        }
		String defaultError =  rule.getPath() + " " + rule.getType();
		String modelMessageKey = getMessageResolver(rootModel, rule.getPath(), true);
        String ruleArg = getMessageResolver(rootModel, rule.getValue(), false);
		
		MessageSourceResolvable modelMessageResolvable = 
			new DefaultMessageSourceResolvable(new String[] {modelMessageKey}, modelMessageKey);
		MessageSourceResolvable argumentMessageResolvable = 
			new DefaultMessageSourceResolvable(new String[] {ruleArg}, ruleArg);
		
		errors.rejectValue(errorMessagePath, errorMessageKey, 
				new Object[] {modelMessageResolvable, argumentMessageResolvable}, defaultError);
	}
	
	protected String getMessageResolver(Object model, String rulePath, boolean resolveAsModel) {
		// if there is no path, return.
		if (rulePath == null || rulePath.length() < 1) {
			return rulePath;
		} else if (isEL(rulePath)) {
			// If the path is actually an expression language statement
			// Need to check if it resolves to a path on the model.
			// trim off EL denotation #{}
			String expressionlessValue = rulePath.substring(2, rulePath.length() - 1);
			// trim off any possible model prefix e.g. model.path.field
			if (expressionlessValue.startsWith("model.")) {
				expressionlessValue = expressionlessValue.substring(6);
			}
			// check if path matches a path on the model.
			if (new BeanWrapperImpl(model).isReadableProperty(expressionlessValue)) {
				// Since this matched a model path, get the label 
				// for the resolved model.
				return getModelMessageKey(expressionlessValue, model);
			} else {
				// It's not a model object, so we don't need the label message key.
				// Instead, use the value of the expression as a label.
				// If the expression fails, just use the expression itself.
				return String.valueOf(getContextArgument(model, rulePath));
			}
		} else {
			if (resolveAsModel) {
				// not an expression, just get the model message key.
				return getModelMessageKey(rulePath, model);
			} else {
				// not an expression, return literal
				return rulePath;
			}
		}
	}
	
	protected String getModelMessageKey(String rulePath, Object rootModel) {
		
		if (rulePath == null || rulePath.length() < 1) {
			return rulePath;
		}		

		Object parent = null;
		String fieldPath = null;
		
		if (rulePath.contains(".")) {
			fieldPath = rulePath.substring(rulePath.lastIndexOf(".") + 1);
			String parentPath = rulePath.substring(0, rulePath.lastIndexOf("."));
			BeanWrapperImpl beanWrapper = new BeanWrapperImpl(rootModel);
			parent = beanWrapper.getPropertyValue(parentPath);
		} else {
			fieldPath = rulePath;
			parent = rootModel;
		}
		
		return StringUtils.uncapitalize(parent.getClass().getSimpleName()) + "." + fieldPath;		
	}
	
	protected boolean isEL(String expression) {
		return expression.matches("\\$\\{.+\\}");
	}
	
	@SuppressWarnings("deprecation")
	protected Object resolveEL(String el, Object model) {
		ExpressionEvaluator evaluator = new ExpressionEvaluatorImpl();
		VariableResolver vResolver = getVariableResolver(model);
		FunctionMapper fMapper = null;
		Class expectedType = Object.class;
		Object result = null;
		try {
			result = evaluator.evaluate(el, expectedType, vResolver, fMapper);
		} catch (ELException ele) {
			log.error("Could not evaluate EL statement: ", ele);
		}
		return result;
	}
	
	protected abstract VariableResolver getVariableResolver(Object model);
	
	protected String appendPath(String path, String suffix) {
		String newPath = path + "." + suffix;
		if (newPath.startsWith(".")) {
			newPath = newPath.substring(1);
		}
		return newPath;
	}
	
}
